﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the Microsoft Public License (Ms-PL).
// See accompanying file License.txt or copy at http://mews.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Mews.Mime
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.CodeAnalysis;
    using System.Linq;

    /// <summary>Extracts the header fields from a <a href="http://www.ietf.org/rfc/rfc5322.txt">RFC 5322</a> message.
    /// </summary>
    /// <typeparam name="TEntity">An <see cref="Entity"/> subclass.</typeparam>
    internal sealed class HeaderProcessor<TEntity> : ILineProcessor<TEntity> where TEntity : Entity<TEntity>, new()
    {
        private readonly List<char> currentFieldName = new List<char>(64);
        private State state;
        private bool inQuotedString;
        private int inComment;
        private HeaderFieldBodyProcessor currentFieldBodyProcessor;
        private readonly TEntity entity;

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>See <see cref="ILineProcessor{T}.ProcessFirstLine"/>.</summary>
        /// <remarks>
        /// <para>The implementation has been put into one function rather than several because performance is better
        /// this way, especially on the emulator. What had been distinct methods earlier are now case statements in the
        /// outer switch block, so maintainability doesn't suffer much. The performance increase is mainly due to the
        /// fact that the number of function calls has been reduced. Additonal improvements would have been possible by
        /// forwarding array segments to the <see cref="HeaderFieldBodyProcessor"/> methods instead of single bytes.
        /// However, an attempt to do so has proven to increase the complexity of this function quite a bit, so it was
        /// decided to postpone further optimizations until this area proves to be a bottleneck.</para>
        /// <para>Note that a reduction of the number of method calls would be especially noticeable when this library
        /// runs on the .NET compact framework as the CF JIT is notoriously bad at inlining and method calls generally
        /// incur more overhead than on the desktop.</para>
        /// </remarks>
        [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Putting the whole state machine into one method improves performance by >15% in the emulator.")]
        public int ProcessFirstLine(ArraySegment<byte> segment)
        {
            unchecked
            {
                int index = segment.Offset;
                int pastEnd = segment.Offset + segment.Count;
                byte currentByte;

                while ((index < pastEnd) &&
                    (((currentByte = segment.Array[index]) != (byte)'\r') || (index == segment.Offset)))
                {
                    switch (this.state)
                    {
                        case State.AtHeaderStart:
                            switch (currentByte)
                            {
                                case (byte)'\r':
                                case (byte)'\n':
                                    // Due to the way processing is split between the different processors, a header can
                                    // start with one superfluous "\r\n" sequence. This actually ignores all line
                                    // breaks.
                                    break;
                                default:
                                    this.state = State.InFieldName;
                                    continue;
                            }

                            break;
                        case State.InFieldName:
                            switch (currentByte)
                            {
                                case (byte)':':
                                    this.state = State.InLeadingBodyWhitespace;
                                    this.currentFieldBodyProcessor = HeaderFieldBodyProcessor.GetProcessor(
                                        new string(this.currentFieldName.ToArray()));
                                    this.currentFieldName.Clear();
                                    break;
                                default:
                                    this.currentFieldName.Add((char)currentByte);
                                    break;
                            }

                            break;
                        case State.InLeadingBodyWhitespace:
                            switch (currentByte)
                            {
                                case (byte)' ':
                                case (byte)'\t':
                                    break;
                                default:
                                    this.state = State.InBodyNormal;
                                    continue;
                            }

                            break;
                        case State.InBodyNormal:
                            switch (currentByte)
                            {
                                case (byte)'\r':
                                    this.state = State.LastWasCarriageReturn;
                                    break;
                                case (byte)'"':
                                    if (this.inComment > 0)
                                    {
                                        this.currentFieldBodyProcessor.ProcessCommentBodyEvent(currentByte);
                                    }
                                    else
                                    {
                                        this.inQuotedString = !this.inQuotedString;
                                        this.currentFieldBodyProcessor.ProcessQuotedBodyEvent(currentByte, true);
                                    }

                                    break;
                                case (byte)'(':
                                    if (this.inQuotedString)
                                    {
                                        this.currentFieldBodyProcessor.ProcessQuotedBodyEvent(currentByte, false);
                                    }
                                    else
                                    {
                                        ++this.inComment;
                                        this.currentFieldBodyProcessor.ProcessCommentBodyEvent(currentByte);
                                    }

                                    break;
                                case (byte)')':
                                    if (this.inQuotedString)
                                    {
                                        this.currentFieldBodyProcessor.ProcessQuotedBodyEvent(currentByte, false);
                                    }
                                    else if (this.inComment > 0)
                                    {
                                        --this.inComment;
                                        this.currentFieldBodyProcessor.ProcessCommentBodyEvent(currentByte);
                                    }
                                    else
                                    {
                                        this.currentFieldBodyProcessor.ProcessNormalBodyEvent(currentByte);
                                    }

                                    break;
                                case (byte)'\\':
                                    if (this.inQuotedString || (this.inComment > 0))
                                    {
                                        this.currentFieldBodyProcessor.ProcessQuotedBodyEvent(currentByte, true);
                                        this.state = State.InBodyQuotedPair;
                                    }
                                    else
                                    {
                                        this.currentFieldBodyProcessor.ProcessNormalBodyEvent(currentByte);
                                    }

                                    break;
                                default:
                                    if (this.inQuotedString)
                                    {
                                        this.currentFieldBodyProcessor.ProcessQuotedBodyEvent(currentByte, false);
                                    }
                                    else if (this.inComment > 0)
                                    {
                                        this.currentFieldBodyProcessor.ProcessCommentBodyEvent(currentByte);
                                    }
                                    else
                                    {
                                        this.currentFieldBodyProcessor.ProcessNormalBodyEvent(currentByte);
                                    }

                                    break;
                            }

                            break;
                        case State.InBodyQuotedPair:
                            if (this.inQuotedString)
                            {
                                this.currentFieldBodyProcessor.ProcessQuotedBodyEvent(currentByte, false);
                            }
                            else
                            {
                                this.currentFieldBodyProcessor.ProcessCommentBodyEvent(currentByte);
                            }

                            this.state = State.InBodyNormal;
                            break;
                        case State.LastWasCarriageReturn:
                            this.state = State.LastWasLineFeed;
                            break;
                        default: // This should be State.LastWasLineFeed, but doing so decreases code coverage
                            switch (currentByte)
                            {
                                case (byte)' ':
                                case (byte)'\t':
                                    // Unfortunately many clients seem to replace spaces with TABs during header
                                    // folding, although the RFC is very clear how such folding must be undone (the CRLF
                                    // before the WSP is simply removed, no matter whether the WSP is a TAB or a space).
                                    // So, client software that follows the RFC to the letter will e.g. often present
                                    // subject lines with TABs in them. These are displayed as real TABs at best and not
                                    // at all at worst. So, it seems, the best solution is to simply convert TABs used
                                    // in header folding to spaces. See http://tools.ietf.org/rfc/rfc5322.txt, 2.2.3.
                                    segment.Array[index] = (byte)' ';
                                    this.state = State.InBodyNormal;
                                    continue;
                                default:
                                    this.AddCurrentFieldIfValid();
                                    this.state = State.InFieldName;

                                    // RFC 5322 seems to be silent on what should happen if we encounter a real line
                                    // break inside a quoted string or a comment. I *guess* the best we can do is to
                                    // treat the quoted string or comment as closed and assume a new header line is
                                    // started.
                                    this.inQuotedString = false;
                                    this.inComment = 0;
                                    continue;
                            }
                    }

                    ++index;
                }

                return index;
            }
        }

        /// <summary>See <see cref="ILineProcessor{T}.GetResult"/>.</summary>
        public TEntity GetResult()
        {
            this.AddCurrentFieldIfValid();
            return this.entity;
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        internal HeaderProcessor()
        {
            this.entity = new TEntity();
            this.entity.Fields.Clear(); // Remove the MIME-Version field that a new Message contains after construction.
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private void AddCurrentFieldIfValid()
        {
            if (this.currentFieldBodyProcessor != null)
            {
                this.entity.Fields.Add(this.currentFieldBodyProcessor.GetHeaderField());
                this.currentFieldBodyProcessor = null;
            }
        }

        private enum State
        {
            AtHeaderStart,
            InFieldName,
            InLeadingBodyWhitespace,
            InBodyNormal,
            InBodyQuotedPair,
            LastWasCarriageReturn,
            LastWasLineFeed
        }
    }
}
